#include <iostream>
#include <cstring>
#include <chrono>
#include <glm/mat4x4.hpp>
#include <glm/gtx/transform.hpp>
#include "VK_UniformBuffer.h"
#include "VK_Context.h"
#include "VK_Image.h"
#include "VK_Texture.h"
#include "VK_DynamicState.h"
#include "VK_CommandPool.h"
#include "VK_Pipeline.h"
#include "VK_Window.h"
#include "../Node.h"
#include "../ObjectFactory.h"

using namespace std;

class mesh: public Node
{
public:
    mesh();
protected:
    RunningState run()override;
    uint32_t updateUniformBufferData(char *&data, uint32_t size);
protected:
    VK_Context *context = nullptr;
    VK_Pipeline *pipeline = nullptr;

    double current = 0.0;
    uint32_t currentWidthIndex = 0;
};

mesh::mesh()
{
    nodeName = "mesh";
}

RunningState mesh::run()
{
    auto window = VK_Window::getInstance()->getWindow();
    auto title = getNodeTitle();
    glfwSetWindowTitle(window, title.data());

    context = createVkContext();

    VK_Context::VK_Config vkConfig;
    bool ok = context->initVulkanDevice(vkConfig);
    if (!ok)
        return RunningState_Failed;

    auto shaderSet = context->createShaderSet();
    shaderSet->addShader("../shader/model-mesh/vert.spv", VK_SHADER_STAGE_VERTEX_BIT);
    shaderSet->addShader("../shader/model-mesh/frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT);

    shaderSet->appendVertexAttributeDescription(0, sizeof (float) * 3, VK_FORMAT_R32G32B32_SFLOAT, 0);
    shaderSet->appendVertexAttributeDescription(1, sizeof (float) * 2, VK_FORMAT_R32G32_SFLOAT,
                                                sizeof(float) * 3);
    shaderSet->appendVertexAttributeDescription(2, sizeof (float) * 3, VK_FORMAT_R32G32B32_SFLOAT,
                                                sizeof(float) * 5);

    shaderSet->appendVertexInputBindingDescription(8 * sizeof(float), 0, VK_VERTEX_INPUT_RATE_VERTEX);

    VkDescriptorSetLayoutBinding uniformBinding = VK_ShaderSet::createDescriptorSetLayoutBinding(0,
                                                                                                 VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_VERTEX_BIT);
    shaderSet->addDescriptorSetLayoutBinding(uniformBinding);

    if (!shaderSet->isValid()) {
        std::cerr << "invalid shaderSet" << std::endl;
        shaderSet->release();
        context->release();
        return RunningState_Failed;
    }

    auto ubo = shaderSet->addUniformBuffer(0, sizeof(float) * 17);
    ubo->setWriteDataCallback([&](char *&data, uint32_t size)->uint32_t{return updateUniformBufferData(data, size);});

    context->initVulkanContext();

    pipeline = context->createPipeline(shaderSet);

    auto rasterCreateInfo = pipeline->getRasterizationStateCreateInfo();
    rasterCreateInfo.polygonMode = VK_POLYGON_MODE_LINE;
    pipeline->setRasterizationStateCreateInfo(rasterCreateInfo);

    auto dynamicState = pipeline->getDynamicState();
    dynamicState->addDynamicState(VK_DYNAMIC_STATE_LINE_WIDTH);
    dynamicState->applyDynamicLineWidth(0.3f);

    pipeline->create();

    auto buffer = context->createVertexBuffer("../model/pug.obj", true);
    pipeline->addRenderBuffer(buffer);

    context->createCommandBuffers();

    current = glfwGetTime();
    auto result = context->run();
    setAverageFps(context->getFps());
    context->release();
    return result;
}

uint32_t mesh::updateUniformBufferData(char *&data, uint32_t size)
{
    static auto startTime = std::chrono::high_resolution_clock::now();
    auto currentTime = std::chrono::high_resolution_clock::now();
    float time = std::chrono::duration<float, std::chrono::seconds::period>
                 (currentTime - startTime).count();
    glm::mat4 model = glm::rotate(glm::mat4(1.0f), glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
    model *= glm::rotate(glm::mat4(1.0f), time * glm::radians(30.0f), glm::vec3(0.0f, 1.0f, 0.0f));
    auto view = glm::lookAt(glm::vec3(0.0f, 4.0f, 0.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f,
                                                                                                0.0f, 1.0f));
    auto window = VK_Window::getInstance()->getWindow();
    int32_t width, height;
    glfwGetFramebufferSize(window, &width, &height);

    auto proj = glm::perspective(glm::radians(45.0f),
                                 (float)width / (float)height, 0.1f, 10.0f);

    proj[1][1] *= -1;
    model = proj * view * model;
    memcpy(data, &model[0][0], size);
    time = sin(time);

    memcpy(data + sizeof(float) * 16, (void *)&time, sizeof(float));

    float widths[] = {0.5f, 1.0f, 1.5f, 2.0f, 2.5f, 3.0f};
    double newTime = glfwGetTime();
    if (newTime > current + 3.0) {
        currentWidthIndex ++;
        if (currentWidthIndex > 5)
            currentWidthIndex = 0;
        auto dynamicState = pipeline->getDynamicState();
        dynamicState->applyDynamicLineWidth(widths[currentWidthIndex]);
        current = newTime;
    }

    return 17 * sizeof(float);
}

REGISTER_OBJECT(Node, mesh)
